﻿// ===============================================================================
//  Copyright © ymind.net. All rights reserved .
//  官方网站：http://ymind.net/
//  版权所有：彦铭工作室 老陈出品
// ===============================================================================

using System;
using System.IO;
using System.Net;
using System.Text;
using Lcsoft.RssEngine.Collections;
using Lcsoft.RssEngine.Shared;

namespace Lcsoft.RssEngine
{
	/// <summary>The contents of a RssFeed</summary>
	[Serializable]
	public class RssFeed
	{
		private ExceptionCollection _exceptions;

		/// <summary>Initialize a new instance of the RssFeed class.</summary>
		public RssFeed()
		{
			this.Url = String.Empty;
			this.LastModified = RssDefault.DateTimeValue;
			this.ETag = String.Empty;
			this.Version = RssVersion.Empty;
			this.Modules = new RssModuleCollection();
			this.Channels = new RssChannelCollection();
		}

		/// <summary>Initialize a new instance of the RssFeed class with a specified encoding.</summary>
		public RssFeed(Encoding encoding)
		{
			this.Url = String.Empty;
			this.LastModified = RssDefault.DateTimeValue;
			this.ETag = String.Empty;
			this.Version = RssVersion.Empty;
			this.Modules = new RssModuleCollection();
			this.Channels = new RssChannelCollection();
			this.Encoding = encoding;
		}

		/// <summary>The channels that are contained in the feed.</summary>
		public RssChannelCollection Channels { get; private set; }

		/// <summary>The modules that the feed adhears to.</summary>
		public RssModuleCollection Modules { get; private set; }

		/// <summary>A collection of all exceptions encountered during the reading of the feed.</summary>
		public ExceptionCollection Exceptions
		{
			get { return this._exceptions ?? new ExceptionCollection(); }
		}

		/// <summary>The Version of the feed.</summary>
		public RssVersion Version { get; set; }

		/// <summary>The server generated hash of the feed.</summary>
		public string ETag { get; private set; }

		/// <summary>The server generated last modfified date and time of the feed.</summary>
		public DateTime LastModified { get; private set; }

		/// <summary>Indicates this feed has not been changed on the server, and the local copy was returned.</summary>
		public bool Cached { get; private set; }

		/// <summary>Location of the feed</summary>
		public string Url { get; private set; }

		/// <summary>Encoding of the feed</summary>
		public Encoding Encoding { get; set; }

		/// <summary>Returns a string representation of the current Object.</summary>
		/// <returns>The Url of the feed</returns>
		public override string ToString() { return this.Url; }

		/// <summary>Reads the specified RSS feed</summary>
		/// <param name="url">The url or filename of the RSS feed</param>
		/// <returns>The contents of the feed</returns>
		public static RssFeed Read(string url) { return _Read(url, null, null); }

		/// <summary>Reads the specified RSS feed</summary>
		/// <param name="request">The specified way to connect to the web server</param>
		/// <returns>The contents of the feed</returns>
		public static RssFeed Read(HttpWebRequest request) { return _Read(request.RequestUri.ToString(), request, null); }

		/// <summary>Reads the specified RSS feed</summary>
		/// <param name="oldFeed">The cached version of the feed</param>
		/// <returns>The current contents of the feed</returns>
		/// <remarks>Will not download the feed if it has not been modified</remarks>
		public static RssFeed Read(RssFeed oldFeed) { return _Read(oldFeed.Url, null, oldFeed); }

		/// <summary>Reads the specified RSS feed</summary>
		/// <param name="request">The specified way to connect to the web server</param>
		/// <param name="oldFeed">The cached version of the feed</param>
		/// <returns>The current contents of the feed</returns>
		/// <remarks>Will not download the feed if it has not been modified</remarks>
		public static RssFeed Read(HttpWebRequest request, RssFeed oldFeed) { return _Read(oldFeed.Url, request, oldFeed); }

		private static RssFeed _Read(string url, HttpWebRequest request, RssFeed oldFeed)
		{
			// ***** Marked for substantial improvement
			var feed = new RssFeed();
			Stream stream = null;
			var uri = new Uri(url);
			feed.Url = url;

			switch (uri.Scheme)
			{
				case "file":
					feed.LastModified = File.GetLastWriteTime(url);
					if ((oldFeed != null) && (feed.LastModified == oldFeed.LastModified))
					{
						oldFeed.Cached = true;
						return oldFeed;
					}
					stream = new FileStream(url, FileMode.Open, FileAccess.Read, FileShare.ReadWrite);
					break;
				case "https":
					goto case "http";
				case "http":
					if (request == null) request = (HttpWebRequest)WebRequest.Create(uri);
					if (oldFeed != null)
					{
						request.IfModifiedSince = oldFeed.LastModified;
						request.Headers.Add("If-None-Match", oldFeed.ETag);
					}
					try
					{
						var response = (HttpWebResponse)request.GetResponse();
						feed.LastModified = response.LastModified;
						feed.ETag = response.Headers["ETag"];
						try
						{
							if (response.ContentEncoding != String.Empty) feed.Encoding = Encoding.GetEncoding(response.ContentEncoding);
						}
						catch {}
						stream = response.GetResponseStream();
					}
					catch (WebException)
					{
						if (oldFeed != null)
						{
							oldFeed.Cached = true;
							return oldFeed;
						}

						throw; // bad
					}
					break;
			}

			if (stream != null)
			{
				RssReader reader = null;
				try
				{
					reader = new RssReader(stream);
					IRssElement element;
					do
					{
						element = reader.Read();

						var item = element as RssChannel.RssChannel;

						if (item != null) feed.Channels.Add(item);
					}
					while (element != null);

					feed.Version = reader.Version;
				}
				finally
				{
					if (reader != null)
					{
						feed._exceptions = reader.Exceptions;
						reader.Close();
					}
				}
			}
			else throw new ApplicationException("Not a valid Url");

			return feed;
		}

		/// <summary>Writes the RSS feed to the specified stream.</summary>
		/// <param name="stream">specified Stream</param>
		/// <exception cref="ArgumentException">The Stream cannot be written to.</exception>
		/// <exception cref="InvalidOperationException">Feed must contain at least one channel.</exception>
		/// <exception cref="InvalidOperationException">Channel must contain at least one item.</exception>
		public void Write(Stream stream)
		{
			var writer = this.Encoding == null ? new RssWriter(stream) : new RssWriter(stream, this.Encoding);

			this._Write(writer);
		}

		/// <summary>Writes the RSS feed to the specified file.</summary>
		/// <remarks>The encoding is UTF8.</remarks>
		/// <exception cref="ArgumentException">The filename is empty, contains only white space, or contains one or more invalid characters.</exception>
		/// <exception cref="UnauthorizedAccessException">Access is denied.</exception>
		/// <exception cref="ArgumentNullException">The filename is a (null c#, Nothing vb) reference.</exception>
		/// <exception cref="DirectoryNotFoundException">The directory to write to is not found.</exception>
		/// <exception cref="IOException">The filename includes an incorrect or invalid syntax for file name, directory name, or volume label syntax.</exception>
		/// <exception cref="System.Security.SecurityException">The caller does not have the required permission.</exception>
		/// <param name="fileName">specified file (including path) If the file exists, it will be truncated with the new content.</param>
		/// <exception cref="InvalidOperationException">Feed must contain at least one channel.</exception>
		/// <exception cref="InvalidOperationException">Channel must contain at least one item.</exception>
		public void Write(string fileName)
		{
			var writer = new RssWriter(fileName);
			this._Write(writer);
		}

		private void _Write(RssWriter writer)
		{
			try
			{
				if (this.Channels.Count == 0) throw new InvalidOperationException("Feed must contain at least one channel.");

				writer.Version = this.Version;
				writer.Modules = this.Modules;

				foreach (var channel in this.Channels)
				{
					if (channel.Items.Count == 0) throw new InvalidOperationException("Channel must contain at least one item.");

					writer.Write(channel);
				}
			}
			finally
			{
				if (writer != null) writer.Close();
			}
		}
	}
}
